package crmdna.attendance;

import static crmdna.common.OfyService.ofy;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

import com.googlecode.objectify.Work;

import crmdna.client.Client;
import crmdna.common.APIException;
import crmdna.common.APIResponse.Status;
import crmdna.common.Utils;
import crmdna.counter.Counter;
import crmdna.counter.Counter.CounterType;
import crmdna.member.Member;
import crmdna.member.MemberCore.MemberProp;
import crmdna.program.Program;
import crmdna.program.ProgramProp;
import crmdna.user.User;
import crmdna.user.User.Action;
import crmdna.user.User.ResourceType;

public class AttendanceDefaultImpl implements IAttendance {

	protected String client;

	AttendanceDefaultImpl(String client) {
		Client.ensureValidClient(client);
		this.client = client;
	}

	@Override
	public int checkin(final long memberId, final long programId,
			final int sessionDateYYYYMMDD, final int batchNo, final String login) {

		ensureValidCheckInInputs(memberId, programId, sessionDateYYYYMMDD,
				batchNo, login);

		final String key = getCheckInKey(programId, memberId);

		CheckInProp checkInProp = ofy(client).transact(new Work<CheckInProp>() {

			@Override
			public CheckInProp run() {
				CheckInEntity checkInEntity = ofy(client).load()
						.type(CheckInEntity.class).id(key).get();

				if (null == checkInEntity) {
					checkInEntity = new CheckInEntity();
					checkInEntity.key = key;
					checkInEntity.programId = programId;
					checkInEntity.memberId = memberId;
				} else {
					CheckInRecord existing = checkInEntity.toProp()
							.getCheckInRecord(sessionDateYYYYMMDD);
					if ((null != existing) && (existing.isCheckin)) {
						String errMessage = "Already checked in by "
								+ existing.login
								+ " "
								+ Utils.getDateDiff(
										existing.timestamp.getTime() / 1000,
										new Date().getTime() / 1000) + " ago";
						throw new APIException(
								Status.ERROR_RESOURCE_ALREADY_EXISTS,
								errMessage);
					}
				}

				CheckInRecord record = new CheckInRecord();
				record.batchNo = batchNo;
				record.isCheckin = true;
				record.login = login;
				record.sessionDateYYYYMMDD = sessionDateYYYYMMDD;
				record.timestamp = new Date();

				checkInEntity.checkInRecords.add(record);
				ofy(client).save().entity(checkInEntity).now();

				return checkInEntity.toProp();
			}
		});

		ProgramProp programProp = Program.safeGet(client, programId).toProp();
		if (checkInProp.getNumCheckins() == programProp.getNumSessions())
			Member.addOrDeleteProgram(client, memberId, programId, true, login);
		else {
			Member.addOrDeleteProgram(client, memberId, programId, false, login);
		}

		String counterKey = getCheckInCounterKey(programId,
				sessionDateYYYYMMDD, batchNo);
		long numCheckIns = Counter.incrementAndGetCurrentCount(client,
				CounterType.CHECKIN, counterKey);
		return (int) numCheckIns;
	}

	@Override
	public int checkout(final long memberId, final long programId,
			final int sessionDateYYYYMMDD, final String login) {

		ensureValidCheckInInputs(memberId, programId, sessionDateYYYYMMDD, 1,
				login);

		final String key = getCheckInKey(programId, memberId);

		Integer batchNo = ofy(client).transact(new Work<Integer>() {

			@Override
			public Integer run() {
				CheckInEntity checkInEntity = ofy(client).load()
						.type(CheckInEntity.class).id(key).get();

				if (null == checkInEntity)
					throw new APIException(
							Status.ERROR_RESOURCE_DOES_NOT_EXIST, "Member ["
									+ memberId + "] is not checked in");

				CheckInRecord existing = checkInEntity.toProp()
						.getCheckInRecord(sessionDateYYYYMMDD);
				if (null == existing)
					throw new APIException(
							Status.ERROR_RESOURCE_DOES_NOT_EXIST, "Member ["
									+ memberId + "] is not checked in");

				if (!existing.isCheckin) {
					String errMessage = "Already checked out by "
							+ existing.login
							+ " "
							+ Utils.getDateDiff(
									existing.timestamp.getTime() / 1000,
									new Date().getTime() / 1000) + " ago";
					throw new APIException(
							Status.ERROR_RESOURCE_DOES_NOT_EXIST, errMessage);
				}

				CheckInRecord record = new CheckInRecord();
				record.batchNo = existing.batchNo;
				record.isCheckin = false;
				record.login = login;
				record.sessionDateYYYYMMDD = sessionDateYYYYMMDD;
				record.timestamp = new Date();

				checkInEntity.checkInRecords.add(record);
				ofy(client).save().entity(checkInEntity);

				return existing.batchNo;
			}
		});

		// remove the program from member
		Member.addOrDeleteProgram(client, memberId, programId, false, login);

		String counterKey = getCheckInCounterKey(programId,
				sessionDateYYYYMMDD, batchNo);
		long numCheckIns = Counter.incrementAndGetCurrentCount(client,
				CounterType.CHECKIN, counterKey, -1);
		return (int) numCheckIns;
	}

	@Override
	public int getNumCheckins(long programId, int sessionDateYYYYMMDD,
			int batchNo) {

		ensureValidCheckInInputs(programId, sessionDateYYYYMMDD, batchNo);
		String counterKey = getCheckInCounterKey(programId,
				sessionDateYYYYMMDD, batchNo);

		long numCheckIns = Counter.getCount(client, CounterType.CHECKIN,
				counterKey);
		return (int) numCheckIns;
	}

	public List<CheckInStatusProp> getCheckInStatus(long programId,
			List<Long> memberIds, int sessionDateYYYYMMDD) {

		List<CheckInStatusProp> result = new ArrayList<>();
		// result will have the same size as memberIds
		List<String> checkInKeys = new ArrayList<>();

		for (int i = 0; i < memberIds.size(); i++) {
			CheckInStatusProp checkInStatusProp = new CheckInStatusProp();
			checkInStatusProp.checkedIn = false;
			checkInStatusProp.userFriendlyMessage = "Not checked in";
			result.add(checkInStatusProp);
			checkInKeys.add(getCheckInKey(programId, memberIds.get(i)));
		}

		Map<String, CheckInEntity> map = ofy(client).load()
				.type(CheckInEntity.class).ids(checkInKeys);

		for (int i = 0; i < memberIds.size(); i++) {
			String checkinKey = checkInKeys.get(i);

			if (map.containsKey(checkinKey)) {
				CheckInEntity checkInEntity = map.get(checkinKey);

				if (checkInEntity != null) {
					CheckInRecord existing = checkInEntity.toProp()
							.getCheckInRecord(sessionDateYYYYMMDD);
					if ((existing != null) && existing.isCheckin) {
						CheckInStatusProp checkInStatusProp = result.get(i);
						checkInStatusProp.checkedIn = true;
						checkInStatusProp.userFriendlyMessage = "Checked in by "
								+ existing.login
								+ " "
								+ Utils.getDateDiff(
										existing.timestamp.getTime() / 1000,
										new Date().getTime() / 1000) + " ago";
					}
				}
			}
		}

		return result;
	}

	@Override
	public List<CheckInMemberProp> getMembersForCheckIn(String searchStr,
			long programId, int sessionDateYYYYMMDD, Integer maxResultsSize,
			String login) {

		Client.ensureValidClient(client);
		User.ensureValidUser(client, login);

		if ((searchStr == null) || (searchStr.length() < 3))
			Utils.throwIncorrectSpecException("Atleast 3 chars required for search");

		Program.safeGet(client, programId).toProp();

		// TODO: put an upper limit for results returned in member.query
		List<MemberProp> memberProps = Member.query(client, searchStr, null,
				null, null, login);

		List<CheckInMemberProp> checkInMemberProps = new ArrayList<>();
		List<Long> memberIds = new ArrayList<>();
		for (MemberProp memberProp : memberProps) {
			memberIds.add(memberProp.memberId);
		}

		List<CheckInStatusProp> checkInStatus = getCheckInStatus(programId,
				memberIds, sessionDateYYYYMMDD);

		for (int i = 0; i < memberIds.size(); i++) {

			CheckInMemberProp checkInMemberProp = new CheckInMemberProp();

			MemberProp memberProp = memberProps.get(i);
			checkInMemberProp.memberId = memberProp.memberId;
			checkInMemberProp.name = memberProp.contact.getName();
			checkInMemberProp.email = memberProp.contact.email;
			checkInMemberProp.phoneNos = memberProp.contact.getPhoneNos();
			checkInMemberProp.practiceIds = memberProp.practiceIds;
			checkInMemberProp.allow = true;

			CheckInStatusProp checkInStatusProp = checkInStatus.get(i);
			if (checkInStatusProp.checkedIn) {
				checkInMemberProp.allow = false;
				checkInMemberProp.notAllowingReason = checkInStatusProp.userFriendlyMessage;
			}

			checkInMemberProps.add(checkInMemberProp);
		}

		// TODO: handle maxResultsSize

		// TODO: handle the case where the program requires registration
		// add fields bool requireRegistration and Set<Long>
		// registeredProgramIds in member entity
		return checkInMemberProps;
	}

	protected void ensureValidCheckInInputs(long memberId, long programId,
			int sessionDateYYYYMMDD, int batchNo, String login) {

		ProgramProp programProp = Program.safeGet(client, programId).toProp();

		User.ensureAccess(client, login, ResourceType.GROUP,
				programProp.groupProp.groupId, Action.WRITE);

		Member.safeGet(client, memberId, login);

		Utils.ensureFormatYYYYMMDD(sessionDateYYYYMMDD);

		if ((sessionDateYYYYMMDD < programProp.startYYYYMMDD)
				|| (sessionDateYYYYMMDD > programProp.endYYYYMMDD))
			Utils.throwIncorrectSpecException("session date ["
					+ sessionDateYYYYMMDD
					+ "] should be between program start ["
					+ programProp.startYYYYMMDD + "] and end date ["
					+ programProp.endYYYYMMDD + "]");

		if (batchNo > programProp.numBatches)
			Utils.throwIncorrectSpecException("Batch number [" + batchNo
					+ "] is greater than total number of batches ["
					+ programProp.numBatches + "]");
	}

	protected void ensureValidCheckInInputs(long programId,
			int sessionDateYYYYMMDD, int batchNo) {

		ProgramProp programProp = Program.safeGet(client, programId).toProp();

		Utils.ensureFormatYYYYMMDD(sessionDateYYYYMMDD);

		if ((sessionDateYYYYMMDD < programProp.startYYYYMMDD)
				|| (sessionDateYYYYMMDD > programProp.endYYYYMMDD))
			Utils.throwIncorrectSpecException("session date ["
					+ sessionDateYYYYMMDD
					+ "] should be between program start ["
					+ programProp.startYYYYMMDD + "] and end date ["
					+ programProp.endYYYYMMDD + "]");

		if (batchNo > programProp.numBatches)
			Utils.throwIncorrectSpecException("Batch number [" + batchNo
					+ "] is greater than total number of batches ["
					+ programProp.numBatches + "]");
	}

	protected String getCheckInKey(long programId, long memberId) {
		return programId + "_" + memberId;
	}

	protected String getCheckInCounterKey(long programId,
			long sessionDateYYYYMMDD, int batchNo) {
		return programId + "_" + sessionDateYYYYMMDD + "_" + batchNo;
	}
}
